package pl.jad.database.table;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;

import pl.jad.database.mapper.DbMapper;
import pl.jad.database.table.column.ArrayIndexTableColumn;
import pl.jad.database.table.column.ArrayTableColumn;
import pl.jad.database.table.column.ClassTableColumn;
import pl.jad.database.table.column.IndexTableColumn;
import pl.jad.database.table.column.ObjectTableColumn;
import pl.jad.database.table.column.PrimitiveTableColumn;
import pl.jad.database.table.column.TableArrayTypes;
import pl.jad.database.table.column.TableColumn;
import pl.jad.database.table.row.TableRow;
import pl.jad.database.table.row.TableRows;

/**
 * Reprezentacja tabeli z bazy danych dla klas Java
 * 
 */
public class Table {
	protected String _name;

	protected Logger _Log;

	
	/**
	 * Columns list
	 */
	protected List<TableColumn> _columns = new ArrayList<TableColumn>();

	/**
	 * Index column
	 */
	protected IndexTableColumn _indexColumn;
	
	protected ClassTableColumn _classColumn;
	
	/**
	 * Class tree
	 */
	protected List<Class<?>> _classTree = new ArrayList<Class<?>>();
	
	/**
	 * Main class
	 */
	protected Class<?> _mainClass;
	
	protected DbMapper _mapper;
	

	/**
	 * Constructor Creates table with name corresponding to the specified class.
	 * 
	 * @param Clas
	 * @throws Exception 
	 */
	// TODO: pobierac obiekt ktory ma byc zapisywany - osobny konstruktor?
	public Table(DbMapper mapper, Object object) throws Exception {
		_mapper = mapper;
		construct(object);
	}
	
	public Table(DbMapper mapper, Class<?> Clas) throws Exception {
		_mapper = mapper;
		construct(Clas);
	}
	
	protected void init() {
		if (this._Log == null) {
			this._Log = Logger.getLogger(this.getClass().getName());
		}
	}
	
	protected void construct(Object object) throws Exception {
		init();
		
		if (object.getClass() == ArrayList.class) {
			createArrayListTable(object);
			
			if (this._mainClass == null) {
				throw new Exception("Main class not found. Possible error while creating table.");
			}
			
			_Log.info("Table prepared: " + _name);
		} else {
			construct(object.getClass());
		}
	}
	
	protected void construct(Class<?> Clas) throws Exception {
		init();
		
		if (Clas.isArray()) {
			// array
			_Log.info("Saving array in DB");
			
			createArrayTable(Clas);
		} else if (Clas == ArrayList.class) {			
			throw new IllegalArgumentException("You must use Table(Object) constructor " +
			"for ArrayList");
		} else {
			// "simple" class - default
			_Log.info("Saving simple class in DB");
			
			createSimpleTable(Clas);
		}
		
		if (this._mainClass == null) {
			throw new Exception("Main class not found. Possible error while creating table.");
		}
		
		_Log.info("Table prepared: " + _name);
	}
	
	/**
	 * 
	 * @param Clas array component class name
	 * @throws Exception
	 */
	// TODO: store array object in table Array and sub-tables, e.g. java_lang_String
	protected void createArrayTable(Class<?> Clas) throws Exception {
		this._mainClass = Clas;
		
		this._name = Clas.getComponentType().getName().replace('.', '_');//Array.class.getName().replace('.', '_'); 
		
		this._Log.info("Trying to create array table for class: " + this._name);
		
		// dodanie kolumny z indeksem/kluczem glownym
		addIndexColumn(false);
		
		// dodanie kolumny tabeli reprezentujacej indeks zapisywanej tablicy
		addArrayIndexColumn();
		
		// dodanie kolumny przechowujacej wartosci z tablicy
		addColumn(TableArrayTypes.get(Clas));
	}
	
	protected void createSimpleTable(Class<?> Clas) throws Exception {
		this._Log.fine("Trying to create table for classes begining from: " + Clas.getName());
		
		// TODO: trzeba tworzyc drzewo klas dla takich typow jak Integer?
		// utworzenie listy klas do najbardziej podstawowej
		this.createClassTree(Clas);
		
		// tabela zostanie utworzona z nazwa klasy glownej
		this._name = _mainClass.getName().replace('.', '_');
		
		// dodanie kolumny z indeksem/kluczem glownym
		addIndexColumn(true);	
	
		// dodanie kolumny z nazwa klasy
		addClassColumn();
		
		// utworzenie listy pol znajdujacych sie w drzewie klas
		createColumns(this._classTree);
	}
	
	protected void createArrayListTable(Object object) throws Exception {
		_Log.info("Getting necessary data from ArrayList object");
		
		// ArrayList
		ArrayList<?> list = (ArrayList<?>) object;
		
		Class<?> elementClass;
		
		if (list.size() == 0) {
			throw new IllegalArgumentException("Can not create table from empty " +
					"ArrayList");
		} else {
			elementClass = list.get(0).getClass();
			this._mainClass = elementClass;
			
			_Log.info("ArrayList elements class: " + elementClass.getName());
			
			for (Object element : list) {
				if (elementClass != element.getClass()) {
					throw new IllegalArgumentException("All elements of ArrayList " +
							"must be same class");
				}
			}
		}
		
		this._Log.info("Trying to create ArrayList table for class: " 
				+ elementClass.getName());
		
		// tabela zostanie utworzona z nazwa klasy glownej
		this._name = _mainClass.getName().replace('.', '_');
		
		// dodanie kolumny z indeksem/kluczem glownym
		addIndexColumn(false);
		
		// dodanie kolumny tabeli reprezentujacej indeks zapisywanej tablicy
		addArrayIndexColumn();
		
		// dodanie kolumny przechowujacej wartosci z tablicy
		addColumn(TableArrayTypes.get(elementClass));
	}
	
	/**
	 * Utworzenie listy klas od przekazanej do pierwszej, dziedziczacej po klasie Object
	 * @param Clas
	 * @throws ClassNotFoundException 
	 */
	protected void createClassTree(Class<?> Clas) throws ClassNotFoundException {
		if (Clas == null) {
			throw new NullPointerException("Cannot create class tree for null type");
		}
		
		this._Log.info("Making class tree for: " + Clas.getName());
		
		Class<?> tmpClass = Clas;
		this._Log.info("Class tree adding: " + tmpClass.getName());
		_classTree.add(tmpClass);
		
		while ((tmpClass = tmpClass.getSuperclass()) != Object.class) {
			this._Log.info("Class tree adding: " + tmpClass.getName());
			_classTree.add(tmpClass);
		}
		
		this._mainClass = _classTree.get(_classTree.size()-1);
		
		this._Log.info("Class tree created. Root class is : " 
				+ _mainClass.getName());
		
	}
	
	/**
	 * Creates columns from class tree
	 * @throws ClassNotFoundException 
	 */
	protected void createColumns(List<Class<?>> ClassTree) throws ClassNotFoundException {
		if (ClassTree == null) {
			throw new NullPointerException("List of classes required");
		}
		
		_Log.info("Gattering fields information from class tree");
		
		//List<Field> fields = new ArrayList<Field>();
		Field[] tmpFields;
		for (Class<?> clas : ClassTree) {
			tmpFields = clas.getDeclaredFields();
			for (Field field : tmpFields) {
				//fields.add(field);
				addColumn(field);
			}
		}
		
	}
	
	/**
	 * Adds column to this table
	 * @param field
	 * @throws ClassNotFoundException 
	 */
	protected void addColumn(Field field) throws ClassNotFoundException {
		if (field == null) {
			throw new NullPointerException("Column must point to specified field");
		}
		
		TableColumn column;
		
		if (isPrimitive(field.getType())) {
			// pole to zmienna prymitywna
			column = new PrimitiveTableColumn(this, field);
			this._Log.info("Primitive column added: " + column.getName());
		} else if (field.getType().isArray()) {
			// pole to tablica
			column = new ArrayTableColumn(this, field);
			this._Log.info("Array column added: " + field.getName());
		} else {
			// TODO: add foreign key column
			column = new ObjectTableColumn(this, field);
			this._Log.info("Object column added: " + field.getName());
		}
		
		this._columns.add(column);
	}
	
	protected void addIndexColumn(boolean isPK) throws Exception {
		if (this._indexColumn != null) {
			//throw new Exception("Index column exists");
			_Log.info("Index column already exists");
		} else {
			this._indexColumn = new IndexTableColumn(this);
			this._indexColumn.setIsPrimaryKey(isPK);
			
			this._columns.add(this._indexColumn);
			
			_Log.info("Index column added");
		}
	}
	
	protected void addArrayIndexColumn() throws Exception {
		TableColumn column = new ArrayIndexTableColumn(this);
		this._columns.add(column);
		
		_Log.info("Array index column added");
	}
	
	protected void addClassColumn() throws Exception {
		if (this._classColumn != null) {
			_Log.info("Class column already exists");
		} else {
			this._classColumn = new ClassTableColumn(this);
			this._columns.add(this._classColumn);
			
			_Log.info("Class column added");
		}
	}

	/**
	 * Returns if specified type name is primitive type
	 * 
	 * @param name
	 * @return true if passed name is primitive type name; false if passed name
	 *         is not primitive type name
	 */
	// TODO: optymalizacja - zastosować HashMapę?
	// FIXME: jakie typy poza String powinny byc zaliczane do prymitywnych?
	final public static boolean isPrimitive(Class<?> Clas) {
		String name = Clas.getName();
		String primitives[] = new String[] { "boolean", "char", "byte",
				"short", "int", "long", "float", "double", "".getClass().getName() };

		for (String primitive : primitives) {
			if (name.equals(primitive)) {
				//_Log.info(name + " <- is primitive type");
				
				return true;
			}
		}

		//_Log.info(name + " <- is NOT primitive type");
		return false;
	}

	/**
	 * Adds column to the table
	 * 
	 * @param column
	 */
	protected void addColumn(TableColumn column) {
		this._columns.add(column);
		this._Log.info("Column " + column.getName() + " added to table");
	}

	/**
	 * Returns table columns
	 * 
	 * @return table columns
	 */
	public List<TableColumn> getColumns() {
		return this._columns;
	}

	/**
	 * Returns table name
	 * 
	 * @return table name
	 */
	public String getName() {
		return this._name;
	}

	/**
	 * Returns SQL which will create this table
	 * 
	 * @return SQL for creating this table
	 */
	public String getCreateSql() {
		StringBuffer sb = new StringBuffer();

		sb.append("CREATE TABLE `" + getName() + "` ");
		sb.append(getColumnsSql());

		//this._Log.info(sb.toString());
		return sb.toString();
	}

	/**
	 * Returns SQL for creating all columns
	 * 
	 * @return SQL for creating all columns
	 */
	protected String getColumnsSql() {
		StringBuilder sb = new StringBuilder();
		sb.append("(");

		for (TableColumn column : getColumns()) {
			sb.append("`" + column.getName() + "` " + column.getSqlType() + ", ");
		}

		// remove last ", "
		sb.delete(sb.length() - 2, sb.length());

		sb.append(")");
		return sb.toString();
	}

	public String getInsertSql(Object object) throws Exception {
		if (object.getClass().isArray()) {
			TableRows rows = new TableRows(this, object);
			return rows.getInsertSql();
		} else if (object.getClass() == ArrayList.class) {
			ArrayList<?> list = (ArrayList<?>) object;
			TableRows rows = new TableRows(this, list.toArray());
			return rows.getInsertSql();
		} else {
			TableRow row = new TableRow(this, object);
			return row.getInsertSql();
		}
	}
	
	public List<String> getAlterColumnsSql() {
		String alterStart = "ALTER TABLE `" + this.getName() + "` ADD COLUMN (";
		String alterEnd = ");";
		List<String> sqls = new ArrayList<String>();
		
		String tmp;
		for(TableColumn column : this.getColumns()) {
			tmp = alterStart + "`" + column.getName() + "` " + column.getSqlType() + alterEnd;
			sqls.add(tmp);
		}
		
		return sqls;
	}
	
	public String getAllSql () {
		return "SELECT * FROM `" + getName() + "`;";
	}
	
	public String getSql(int pk) {
		return "SELECT * FROM `" + getName() + "` WHERE `" + _indexColumn.getName() +
			"` = " + pk + ";";
	}
	
	public String getSql(HashMap<String, String> pks) {
		StringBuilder sb = new StringBuilder();
		sb.append("SELECT * FROM `" + getName() + "` WHERE ");
		
		for (String key : pks.keySet()) {
			// TODO: dodac na stale ' ?
			sb.append(" `" + key + "` = " + pks.get(key) + ", ");
		}
		
		sb.delete(sb.length() - 2, sb.length());
		sb.append(";");
		return sb.toString();
	}
	
	public int getLastIndex() {
		return this._indexColumn.getIndex();
	}
	
	public Connection getConnection() {
		return _mapper.getConnection();
	}
	
	public DbMapper getMapper() {
		return _mapper;
	}
	
	public String toString() {
		return getCreateSql();
	}
}
